pomera-ai-commander 1.2.7 → 1.2.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,51 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Pomera AI Commander - Desktop Shortcut Creator
5
+ *
6
+ * This script creates a desktop shortcut for the Pomera GUI.
7
+ */
8
+
9
+ const { spawn } = require('child_process');
10
+ const path = require('path');
11
+
12
+ // Get the path to create_shortcut.py
13
+ const shortcutScript = path.join(__dirname, '..', 'create_shortcut.py');
14
+
15
+ // Find Python executable
16
+ function findPython() {
17
+ const { execSync } = require('child_process');
18
+
19
+ try {
20
+ execSync('python3 --version', { stdio: 'ignore' });
21
+ return 'python3';
22
+ } catch (e) {
23
+ try {
24
+ execSync('python --version', { stdio: 'ignore' });
25
+ return 'python';
26
+ } catch (e) {
27
+ console.error('Error: Python is not installed or not in PATH');
28
+ process.exit(1);
29
+ }
30
+ }
31
+ }
32
+
33
+ const pythonCmd = findPython();
34
+
35
+ // Get command line arguments
36
+ const args = process.argv.slice(2);
37
+
38
+ // Spawn the Python script
39
+ const proc = spawn(pythonCmd, [shortcutScript, ...args], {
40
+ stdio: 'inherit',
41
+ cwd: path.join(__dirname, '..')
42
+ });
43
+
44
+ proc.on('close', (code) => {
45
+ process.exit(code || 0);
46
+ });
47
+
48
+ proc.on('error', (err) => {
49
+ console.error('Failed to create shortcut:', err.message);
50
+ process.exit(1);
51
+ });
package/bin/pomera.js ADDED
@@ -0,0 +1,68 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Pomera AI Commander - GUI launcher
5
+ *
6
+ * This script launches the Pomera GUI application.
7
+ */
8
+
9
+ const { spawn } = require('child_process');
10
+ const path = require('path');
11
+
12
+ // Get the path to pomera.py
13
+ const pomeraPath = path.join(__dirname, '..', 'pomera.py');
14
+
15
+ // Find Python executable
16
+ function findPython() {
17
+ const { execSync } = require('child_process');
18
+
19
+ // Try pythonw first (Windows - no console)
20
+ if (process.platform === 'win32') {
21
+ try {
22
+ execSync('pythonw --version', { stdio: 'ignore' });
23
+ return 'pythonw';
24
+ } catch (e) {
25
+ // Fall through
26
+ }
27
+ }
28
+
29
+ // Try python3 (Linux/macOS)
30
+ try {
31
+ execSync('python3 --version', { stdio: 'ignore' });
32
+ return 'python3';
33
+ } catch (e) {
34
+ // Fall back to python
35
+ try {
36
+ execSync('python --version', { stdio: 'ignore' });
37
+ return 'python';
38
+ } catch (e) {
39
+ console.error('Error: Python is not installed or not in PATH');
40
+ console.error('Please install Python 3.8 or higher');
41
+ process.exit(1);
42
+ }
43
+ }
44
+ }
45
+
46
+ const pythonCmd = findPython();
47
+
48
+ // Spawn the Python GUI
49
+ const app = spawn(pythonCmd, [pomeraPath], {
50
+ stdio: 'inherit',
51
+ cwd: path.join(__dirname, '..'),
52
+ detached: process.platform !== 'win32' // Detach on non-Windows
53
+ });
54
+
55
+ // Handle process exit
56
+ app.on('close', (code) => {
57
+ process.exit(code || 0);
58
+ });
59
+
60
+ // Handle errors
61
+ app.on('error', (err) => {
62
+ console.error('Failed to start Pomera:', err.message);
63
+ process.exit(1);
64
+ });
65
+
66
+ // Forward signals
67
+ process.on('SIGINT', () => app.kill('SIGINT'));
68
+ process.on('SIGTERM', () => app.kill('SIGTERM'));
@@ -398,6 +398,21 @@ class DataTypeConverter:
398
398
  """
399
399
  import json
400
400
 
401
+ # Handle None or empty string for all types
402
+ if value_str is None or value_str == '':
403
+ if data_type in ('json', 'array'):
404
+ return [] if data_type == 'array' else {}
405
+ elif data_type == 'str':
406
+ return ''
407
+ elif data_type == 'int':
408
+ return 0
409
+ elif data_type == 'float':
410
+ return 0.0
411
+ elif data_type == 'bool':
412
+ return False
413
+ else:
414
+ return ''
415
+
401
416
  if data_type == 'str':
402
417
  return value_str
403
418
  elif data_type == 'int':
@@ -407,6 +422,14 @@ class DataTypeConverter:
407
422
  elif data_type == 'bool':
408
423
  return value_str == '1'
409
424
  elif data_type in ('json', 'array'):
410
- return json.loads(value_str)
425
+ # Handle whitespace-only strings as empty
426
+ if not value_str.strip():
427
+ return [] if data_type == 'array' else {}
428
+ try:
429
+ return json.loads(value_str)
430
+ except json.JSONDecodeError as e:
431
+ # Debug: print what value caused the error
432
+ print(f"DEBUG: JSON parse failed for data_type='{data_type}': value_str='{value_str[:100]}...' error={e}")
433
+ return [] if data_type == 'array' else {}
411
434
  else:
412
435
  return value_str # Fallback to string
@@ -308,8 +308,10 @@ class DatabaseSchemaManager:
308
308
  for index_sql in table_indexes:
309
309
  # Extract index name from CREATE INDEX statement
310
310
  parts = index_sql.split()
311
- if len(parts) >= 5 and parts[0].upper() == "CREATE" and parts[1].upper() == "INDEX":
312
- index_name = parts[4] # "CREATE INDEX IF NOT EXISTS index_name"
311
+ # "CREATE INDEX IF NOT EXISTS idx_name ON table(...)"
312
+ # parts: [0]=CREATE [1]=INDEX [2]=IF [3]=NOT [4]=EXISTS [5]=idx_name
313
+ if len(parts) >= 6 and parts[0].upper() == "CREATE" and parts[1].upper() == "INDEX":
314
+ index_name = parts[5] # After "CREATE INDEX IF NOT EXISTS"
313
315
  expected_indexes.add(index_name)
314
316
 
315
317
  # Get existing indexes
@@ -143,6 +143,27 @@ class NestedSettingsProxy:
143
143
  """Return a copy of the underlying data as a regular dictionary."""
144
144
  return self._data.copy()
145
145
 
146
+ def pop(self, key: str, *args):
147
+ """Remove and return a value from the nested settings.
148
+
149
+ Args:
150
+ key: Key to remove
151
+ *args: Optional default value if key not found
152
+
153
+ Returns:
154
+ The removed value, or default if provided and key not found
155
+ """
156
+ if args:
157
+ result = self._data.pop(key, args[0])
158
+ else:
159
+ result = self._data.pop(key)
160
+
161
+ # Save the change to database
162
+ full_path = f"{self._parent_key}"
163
+ self._settings_manager.set_setting(full_path, self._data)
164
+
165
+ return result
166
+
146
167
  def _update_nested_value(self, data: Dict[str, Any], path: str, value: Any) -> None:
147
168
  """Update value in nested dictionary using dot notation."""
148
169
  keys = path.split('.')
@@ -1112,8 +1133,10 @@ class DatabaseSettingsManager:
1112
1133
 
1113
1134
  critical_issues = [i for i in issues if i.severity == 'critical']
1114
1135
  if critical_issues:
1115
- self.logger.error(f"Imported settings have {len(critical_issues)} critical issues")
1116
- return False
1136
+ # Log specific issues for debugging but allow import to proceed
1137
+ for issue in critical_issues:
1138
+ self.logger.warning(f"Import validation issue: {issue.location} - {issue.message}")
1139
+ self.logger.warning(f"Imported settings have {len(critical_issues)} validation issues - proceeding anyway")
1117
1140
 
1118
1141
  # Save imported settings
1119
1142
  success = self.save_settings(settings_data)
@@ -283,8 +283,8 @@ class DialogManager:
283
283
  try:
284
284
  dialog_settings = self.settings_manager.get_setting("dialog_settings", {})
285
285
 
286
- # Validate settings structure
287
- if not isinstance(dialog_settings, dict):
286
+ # Validate settings structure - accept dict or dict-like objects (NestedSettingsProxy)
287
+ if not isinstance(dialog_settings, dict) and not hasattr(dialog_settings, 'get'):
288
288
  self.logger.warning(f"Invalid dialog_settings structure: {type(dialog_settings)}, using defaults")
289
289
  return True
290
290
 
@@ -663,8 +663,8 @@ class DialogManager:
663
663
  # Force a fresh read of dialog settings
664
664
  dialog_settings = self.settings_manager.get_setting("dialog_settings", {})
665
665
 
666
- # Validate settings structure
667
- if not isinstance(dialog_settings, dict):
666
+ # Validate settings structure - accept dict or dict-like objects (NestedSettingsProxy)
667
+ if not isinstance(dialog_settings, dict) and not hasattr(dialog_settings, 'get'):
668
668
  self.logger.error(f"Invalid dialog_settings structure: {type(dialog_settings)}, skipping refresh")
669
669
  return
670
670
 
@@ -9,6 +9,7 @@ from tkinter import scrolledtext
9
9
  import platform
10
10
  import time
11
11
  import threading
12
+ import hashlib
12
13
  from typing import Dict, List, Tuple, Optional, Any
13
14
  from dataclasses import dataclass
14
15
 
@@ -31,7 +32,7 @@ class EfficientLineNumbers(tk.Frame):
31
32
 
32
33
  # Configuration
33
34
  self.line_number_width = 50 # Adjustable width
34
- self.debounce_delay = 5 # ms - very responsive updates
35
+ self.debounce_delay = 50 # ms - balanced responsiveness vs. efficiency
35
36
  self.cache_size_limit = 1000 # Maximum cached line positions
36
37
 
37
38
  # Create widgets
@@ -280,12 +281,12 @@ class EfficientLineNumbers(tk.Frame):
280
281
  return None
281
282
 
282
283
  def _get_content_hash(self) -> str:
283
- """Get a hash of the current content for change detection."""
284
+ """Get a hash of the current content for change detection using MD5."""
284
285
  try:
285
286
  content = self.text.get("1.0", "end-1c")
286
- # Simple hash based on content length and first/last chars
287
287
  if content:
288
- return f"{len(content)}_{content[:10]}_{content[-10:]}"
288
+ # Use MD5 for reliable change detection (truncated for efficiency)
289
+ return hashlib.md5(content.encode('utf-8', errors='replace')).hexdigest()[:16]
289
290
  return "empty"
290
291
  except Exception:
291
292
  return "error"